package com.foodev.maven.gwt;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.LinkedList;

import org.mortbay.component.AbstractLifeCycle;
import org.mortbay.jetty.AbstractConnector;
import org.mortbay.jetty.Request;
import org.mortbay.jetty.RequestLog;
import org.mortbay.jetty.Response;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.handler.RequestLogHandler;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.webapp.Configuration;
import org.mortbay.jetty.webapp.WebAppClassLoader;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.log.Log;
import org.mortbay.log.Logger;

import com.google.gwt.core.ext.ServletContainer;
import com.google.gwt.core.ext.ServletContainerLauncher;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.TreeLogger.Type;
import com.google.gwt.dev.shell.jetty.JettyNullLogger;
import com.google.gwt.dev.util.InstalledHelpInfo;

public class MushedClasspathJettyLauncher extends ServletContainerLauncher {

    /**
     * Log jetty requests/responses to TreeLogger.
     */
    public static class JettyRequestLogger extends AbstractLifeCycle implements RequestLog {

        private final TreeLogger logger;


        public JettyRequestLogger(TreeLogger logger) {
            this.logger = logger;
        }


        /**
         * Log an HTTP request/response to TreeLogger.
         */
        @SuppressWarnings("unchecked")
        public void log(Request request, Response response) {
            int status = response.getStatus();
            if (status < 0) {
                // Copied from NCSARequestLog
                status = 404;
            }
            TreeLogger.Type logStatus, logHeaders;
            if (status >= 500) {
                logStatus = TreeLogger.ERROR;
                logHeaders = TreeLogger.INFO;
            } else if (status >= 400) {
                logStatus = TreeLogger.WARN;
                logHeaders = TreeLogger.INFO;
            } else {
                logStatus = TreeLogger.INFO;
                logHeaders = TreeLogger.DEBUG;
            }
            String userString = request.getRemoteUser();
            if (userString == null) {
                userString = "";
            } else {
                userString += "@";
            }
            String bytesString = "";
            if (response.getContentCount() > 0) {
                bytesString = " " + response.getContentCount() + " bytes";
            }
            TreeLogger branch = logger.branch(logStatus, String.valueOf(status) + " - " + request.getMethod() + ' '
                    + request.getUri() + " (" + userString + request.getRemoteHost() + ')' + bytesString);
            TreeLogger headers = branch.branch(logHeaders, "Request headers");
            Enumeration<String> headerNames = request.getHeaderNames();
            while (headerNames.hasMoreElements()) {
                String hdr = headerNames.nextElement();
                String hdrVal = request.getHeader(hdr);
                headers.log(logHeaders, hdr + ": " + hdrVal);
            }
            // TODO(jat): add response headers
        }
    }


    /**
     * An adapter for the Jetty logging system to GWT's TreeLogger. This implementation class is only public to allow
     * {@link Log} to instantiate it.
     * 
     * The weird static data / default construction setup is a game we play with {@link Log}'s static initializer to
     * prevent the initial log message from going to stderr.
     */
    public static class JettyTreeLogger implements Logger {

        private final TreeLogger logger;


        public JettyTreeLogger(TreeLogger logger) {
            if (logger == null) {
                throw new NullPointerException();
            }
            this.logger = logger;
        }


        public void debug(String msg, Object arg0, Object arg1) {
            logger.log(TreeLogger.SPAM, format(msg, arg0, arg1));
        }


        public void debug(String msg, Throwable th) {
            logger.log(TreeLogger.SPAM, msg, th);
        }


        public Logger getLogger(String name) {
            return this;
        }


        public void info(String msg, Object arg0, Object arg1) {
            logger.log(TreeLogger.INFO, format(msg, arg0, arg1));
        }


        public boolean isDebugEnabled() {
            return logger.isLoggable(TreeLogger.SPAM);
        }


        public void setDebugEnabled(boolean enabled) {
            // ignored
        }


        public void warn(String msg, Object arg0, Object arg1) {
            logger.log(TreeLogger.WARN, format(msg, arg0, arg1));
        }


        public void warn(String msg, Throwable th) {
            logger.log(TreeLogger.WARN, msg, th);
        }


        /**
         * Copied from org.mortbay.log.StdErrLog.
         */
        private String format(String msg, Object arg0, Object arg1) {
            int i0 = msg.indexOf("{}");
            int i1 = i0 < 0 ? -1 : msg.indexOf("{}", i0 + 2);

            if (arg1 != null && i1 >= 0) {
                msg = msg.substring(0, i1) + arg1 + msg.substring(i1 + 2);
            }
            if (arg0 != null && i0 >= 0) {
                msg = msg.substring(0, i0) + arg0 + msg.substring(i0 + 2);
            }
            return msg;
        }
    }


    /**
     * The resulting {@link ServletContainer} this is launched.
     */
    protected static class JettyServletContainer extends ServletContainer {

        private final int actualPort;
        private final File appRootDir;
        private final TreeLogger logger;
        private final Server server;
        private final WebAppContext wac;


        public JettyServletContainer(TreeLogger logger, Server server, WebAppContext wac, int actualPort,
                File appRootDir) {
            this.logger = logger;
            this.server = server;
            this.wac = wac;
            this.actualPort = actualPort;
            this.appRootDir = appRootDir;
        }


        public int getPort() {
            return actualPort;
        }


        public void refresh() throws UnableToCompleteException {
            String msg = "Reloading web app to reflect changes in " + appRootDir.getAbsolutePath();
            TreeLogger branch = logger.branch(TreeLogger.INFO, msg);
            // Temporarily log Jetty on the branch.
            Log.setLog(new JettyTreeLogger(branch));
            try {
                wac.stop();
                wac.start();
                branch.log(TreeLogger.INFO, "Reload completed successfully");
            } catch (Exception e) {
                branch.log(TreeLogger.ERROR, "Unable to restart embedded Jetty server", e);
                throw new UnableToCompleteException();
            } finally {
                // Reset the top-level logger.
                Log.setLog(new JettyTreeLogger(logger));
            }
        }


        public void stop() throws UnableToCompleteException {
            TreeLogger branch = logger.branch(TreeLogger.INFO, "Stopping Jetty server");
            // Temporarily log Jetty on the branch.
            Log.setLog(new JettyTreeLogger(branch));
            try {
                server.stop();
                server.setStopAtShutdown(false);
                branch.log(TreeLogger.INFO, "Stopped successfully");
            } catch (Exception e) {
                branch.log(TreeLogger.ERROR, "Unable to stop embedded Jetty server", e);
                throw new UnableToCompleteException();
            } finally {
                // Reset the top-level logger.
                Log.setLog(new JettyTreeLogger(logger));
            }
        }
    }


    /**
     * A {@link WebAppContext} tailored to GWT hosted mode. Features hot-reload with a new {@link WebAppClassLoader} to
     * pick up disk changes. The default Jetty {@code WebAppContext} will create new instances of servlets, but it will
     * not create a brand new {@link ClassLoader}. By creating a new {@code ClassLoader} each time, we re-read updated
     * classes from disk.
     * 
     * Also provides special class filtering to isolate the web app from the GWT hosting environment.
     */
    protected final class WebAppContextWithReload extends WebAppContext {

        /**
         * Specialized {@link WebAppClassLoader} that allows outside resources to be brought in dynamically from the
         * system path. A warning is issued when this occurs.
         */
        @SuppressWarnings("unused")
        private class WebAppClassLoaderExtension extends WebAppClassLoader {

            private static final String META_INF_SERVICES = "META-INF/services/";


            public WebAppClassLoaderExtension() throws IOException {
                super(bootStrapOnlyClassLoader, WebAppContextWithReload.this);
            }


            @Override
            public URL findResource(String name) {
                // Specifically for META-INF/services/javax.xml.parsers.SAXParserFactory
                String checkName = name;
                if (checkName.startsWith(META_INF_SERVICES)) {
                    checkName = checkName.substring(META_INF_SERVICES.length());
                }

                // For a system path, load from the outside world.
                URL found;
                if (isSystemPath(checkName)) {
                    found = systemClassLoader.getResource(name);
                    if (found != null) {
                        return found;
                    }
                }

                // Always check this ClassLoader first.
                found = super.findResource(name);
                if (found != null) {
                    return found;
                }

                // See if the outside world has it.
                found = systemClassLoader.getResource(name);
                if (found == null) {
                    return null;
                }

                // Warn, add containing URL to our own ClassLoader, and retry the call.
                String warnMessage = "Server resource '" + name
                        + "' could not be found in the web app, but was found on the system classpath";
                if (!addContainingClassPathEntry(warnMessage, found, name)) {
                    return null;
                }
                return super.findResource(name);
            }


            /**
             * Override to additionally consider the most commonly available JSP and XML implementation as system
             * resources. (In fact, Jasper is in gwt-dev via embedded Tomcat, so we always hit this case.)
             */
            @Override
            public boolean isSystemPath(String name) {
                name = name.replace('/', '.');
                return super.isSystemPath(name) || name.startsWith("org.apache.jasper.")
                        || name.startsWith("org.apache.xerces.");
            }


            @Override
            protected Class<?> findClass(String name) throws ClassNotFoundException {
                // For system path, always prefer the outside world.
                if (isSystemPath(name)) {
                    try {
                        return systemClassLoader.loadClass(name);
                    } catch (ClassNotFoundException e) {
                    }
                }

                try {
                    return super.findClass(name);
                } catch (ClassNotFoundException e) {
                    // Don't allow server classes to be loaded from the outside.
                    if (isServerPath(name)) {
                        throw e;
                    }
                }

                // See if the outside world has a URL for it.
                String resourceName = name.replace('.', '/') + ".class";
                URL found = systemClassLoader.getResource(resourceName);
                if (found == null) {
                    return null;
                }

                // Warn, add containing URL to our own ClassLoader, and retry the call.
                String warnMessage = "Server class '" + name
                        + "' could not be found in the web app, but was found on the system classpath";
                if (!addContainingClassPathEntry(warnMessage, found, resourceName)) {
                    throw new ClassNotFoundException(name);
                }
                return super.findClass(name);
            }


            private boolean addContainingClassPathEntry(String warnMessage, URL resource, String resourceName) {
                TreeLogger.Type logLevel = (System.getProperty(PROPERTY_NOWARN_WEBAPP_CLASSPATH) == null) ? TreeLogger.WARN
                        : TreeLogger.DEBUG;
                TreeLogger branch = logger.branch(logLevel, warnMessage);
                String classPathURL;
                String foundStr = resource.toExternalForm();
                if (resource.getProtocol().equals("file")) {
                    assert foundStr.endsWith(resourceName);
                    classPathURL = foundStr.substring(0, foundStr.length() - resourceName.length());
                } else if (resource.getProtocol().equals("jar")) {
                    assert foundStr.startsWith("jar:");
                    assert foundStr.endsWith("!/" + resourceName);
                    classPathURL = foundStr.substring(4, foundStr.length() - (2 + resourceName.length()));
                } else {
                    branch.log(TreeLogger.ERROR, "Found resouce but unrecognized URL format: '" + foundStr + '\'');
                    return false;
                }
                branch = branch.branch(logLevel, "Adding classpath entry '" + classPathURL
                        + "' to the web app classpath for this session", null, new InstalledHelpInfo(
                        "webAppClassPath.html"));
                try {
                    addClassPath(classPathURL);
                    return true;
                } catch (IOException e) {
                    branch.log(TreeLogger.ERROR, "Failed add container URL: '" + classPathURL + '\'', e);
                    return false;
                }
            }
        }


        /**
         * Parent ClassLoader for the Jetty web app, which can only load JVM classes. We would just use
         * <code>null</code> for the parent ClassLoader except this makes Jetty unhappy.
         */
        private final ClassLoader bootStrapOnlyClassLoader = new ClassLoader(null) {
        };

        private final TreeLogger logger;

        /**
         * In the usual case of launching {@link com.google.gwt.dev.HostedMode}, this will always by the system app
         * ClassLoader.
         */
        private final ClassLoader systemClassLoader = Thread.currentThread().getContextClassLoader();


        @SuppressWarnings("unchecked")
        private WebAppContextWithReload(TreeLogger logger, String webApp, String contextPath) {
            super(webApp, contextPath);
            this.logger = logger;

            // Prevent file locking on Windows; pick up file changes.
            getInitParams().put("org.mortbay.jetty.servlet.Default.useFileMappedBuffer", "false");

            // Since the parent class loader is bootstrap-only, prefer it first.
            // XXX Not true. Parent for MushedClasspathJettyLauncher has lots of
            // interesting classes (all project dependencies)
            setParentLoaderPriority(false);
        }


        @Override
        protected void doStart() throws Exception {
            super.doStart();
        }


        @Override
        protected void doStop() throws Exception {
            super.doStop();
            setClassLoader(null);
        }
    }


    /**
     * System property to suppress warnings about loading web app classes from the system classpath.
     */
    private static final String PROPERTY_NOWARN_WEBAPP_CLASSPATH = "gwt.nowarn.webapp.classpath";

    static {
        // Suppress spammy Jetty log initialization.
        System.setProperty("org.mortbay.log.class", JettyNullLogger.class.getName());
        Log.getLog();

        /*
         * Make JDT the default Ant compiler so that JSP compilation just works out-of-the-box. If we don't set this,
         * it's very, very difficult to make JSP compilation work.
         */
        String antJavaC = System.getProperty("build.compiler", "org.eclipse.jdt.core.JDTCompilerAdapter");
        System.setProperty("build.compiler", antJavaC);
    }


    public ServletContainer start(TreeLogger logger, int port, File appRootDir) throws Exception {
        TreeLogger branch = logger.branch(TreeLogger.INFO, "Starting Jetty on port " + port, null);

        checkStartParams(branch, port, appRootDir);

        // Setup our branch logger during startup.
        Log.setLog(new JettyTreeLogger(branch));

        // Turn off XML validation.
        System.setProperty("org.mortbay.xml.XmlParser.Validating", "false");

        AbstractConnector connector = getConnector();
        connector.setPort(port);

        // Don't share ports with an existing process.
        connector.setReuseAddress(false);

        // Linux keeps the port blocked after shutdown if we don't disable this.
        connector.setSoLingerTime(0);

        Server server = new Server();
        server.addConnector(connector);

        // Create a new web app in the war directory.
        WebAppContext wac = buildWebApplicationContext(logger, appRootDir);

        RequestLogHandler logHandler = new RequestLogHandler();
        logHandler.setRequestLog(new JettyRequestLogger(logger));
        logHandler.setHandler(wac);
        server.setHandler(logHandler);
        server.start();
        server.setStopAtShutdown(true);

        // Now that we're started, log to the top level logger.
        Log.setLog(new JettyTreeLogger(logger));

        return new JettyServletContainer(logger, server, wac, connector.getLocalPort(), appRootDir);
    }


    private WebAppContext buildWebApplicationContext(TreeLogger logger, File appRootDir) throws Exception {

        // Create a new web app in the war directory.
        WebAppContext wac = new WebAppContextWithReload(logger, appRootDir.getAbsolutePath(), "/");

        // XXX Not interested in their silly warnings.
        WebAppClassLoader cl = new WebAppClassLoader(Thread.currentThread().getContextClassLoader(), wac);

        // This is written to the startup shell script.
        cl.addClassPath(System.getenv("OUTPUTDIRS"));

        wac.setClassLoader(cl);

        File webInfDir = new File(appRootDir, "WEB-INF");

        File jettyEnvXml = new File(webInfDir, "jetty-env.xml");

        if (jettyEnvXml.isFile()) {

            try {

                Field f = WebAppContext.class.getDeclaredField("__dftConfigurationClasses");
                f.setAccessible(true);

                String[] defaultConfigs = (String[]) f.get(wac);

                LinkedList<String> toLoad = new LinkedList<String>(Arrays.asList(defaultConfigs));
                toLoad.add("org.mortbay.jetty.plus.webapp.EnvConfiguration");

                Configuration[] confs = new Configuration[toLoad.size()];

                int i = 0;
                for (String c : toLoad) {

                    Class<?> configClass = cl.loadClass(c);

                    confs[i++] = (Configuration) configClass.newInstance();

                }

                wac.setConfigurations(confs);

            } catch (ClassNotFoundException cnfe) {
                cnfe.printStackTrace();
                logger.log(Type.WARN, jettyEnvXml.getAbsolutePath() + " found but jetty-plus is not on the classpath. "
                        + "If you want jetty-env.xml to be used, add maven "
                        + "dependency org.mortbay.jetty:jetty-plus:6.1.11");
            }

        }

        return wac;

    }


    protected AbstractConnector getConnector() {
        return new SelectChannelConnector();
    }


    private void checkStartParams(TreeLogger logger, int port, File appRootDir) {
        if (logger == null) {
            throw new NullPointerException("logger cannot be null");
        }

        if (port < 0 || port > 65535) {
            throw new IllegalArgumentException("port must be either 0 (for auto) or less than 65536");
        }

        if (appRootDir == null) {
            throw new NullPointerException("app root direcotry cannot be null");
        }
    }

}